supce's blog

CSS Secret 读书笔记之梯形标签页&饼图


梯形标签页

梯形标签在导航栏部分经常遇到,书中只提到3D旋转的方案,但是万能的裁切和渐变怎么能丢?于是有了下面三种方式。

裁切方案

在形状这部分,裁切是一种很强大的方式,以左上角为原点,梯形的绘制对于它来说很简单。

<div class="echelon-a">this is a test</div>
div{
    margin:20px;
    max-width: 10em;
    min-height: 4em;
    font:125%/4em sans-serif;
    text-align: center;
    color: white;
}
.echelon-a{
    background: #58a;
    -webkit-clip-path: polygon(20% 0,80% 0,100% 100%,0 100%);
}

渐变方案

到现在为止,利用渐变已经实现了条纹背景、复杂背景、图像边框以及下面的梯形。不得不说渐变真是一把利器。只要注意多层渐变的覆盖,设置好每层渐变的大小和位置即可。

.echelon-b{
    background: linear-gradient(120deg,transparent 40px,#58a 0) top left,
                linear-gradient(-120deg,transparent 40px,#58a 0) top right;
    background-size: 50% 100%;
    background-repeat: no-repeat;
}

3D旋转方案

在现实的三维世界中旋转一个矩形,然后其在平面上的投影就是一个梯形。利用这个思路,可以尝试使用3D旋转来模拟一个梯形。
如果对元素直接使用transform: perspective(.5em) rotateX(5deg);,元素内部也会进行旋转,这并不是我们想要的结果:

这时候可以利用伪元素来生成一个矩形,设置其堆叠次序,然后旋转为一个矩形。
仔细观察上面的图还会发现,内部的文字不居中了而且也变矮了,这是由于元素是以自身的中心线为轴进行空间旋转的。因此投影到2D屏幕上尺寸会发生变化,导致高度缩减,宽度增加。
为了控制它的尺寸,这时候可以指定transform-origin为bottom,当它在3D空间旋转时,可以把它的底边固定住。保证宽度不增加,然后再通过变形属性,即scaleY()来保证高度不会变低。
下面是完整的代码:

.echelon-c{
    position: relative;
    display: inline-block;
}
.echelon-c:before{
    content: "";
    position: absolute;
    top: 0;right: 0;bottom: 0;left: 0;
    z-index: -1;
    background: #fb3;
    transform: scaleY(1.7) perspective(.5em) rotateX(5deg);
    transform-origin: bottom;
}

梯形导航

上面说了3种方式主要是用于实现导航,这里就简单模拟了一个梯形的导航。详细过程就不说了,直接上代码。

一定要注意在使用3D旋转时的元素的堆叠顺序。

<nav>
    <a href="#">Home</a>
    <a href="#" class="selected">Contact</a>
    <a href="#">About</a>
</nav>
<main>Content</main>
nav{
    position: relative;
    padding-left: 1em;
}
nav > a {
    position: relative;
    display: inline-block;
    padding: .3em 1em 0;
    color: inherit;
    text-decoration: none;
    margin: 0 -.3em;
}
nav>a::before,main{
    border: .1em solid rgba(0,0,0,.4);
}
nav a::before{
    content: "";
    position: absolute;
    top: 0;right: 0;bottom: 0;left: 0;
    z-index: -1;
    border-bottom: none;
    border-radius: .5em .5em 0 0;
    background: #ccc linear-gradient(hsla(0,0%,100%,.6),hsla(0,0%,100%,0));
    box-shadow: 0 .15em white inset;
    transform: scale(1.1,1.3) perspective(.5em) rotateX(5deg);
    transform-origin: bottom;
}
main {
    width: 30em;
    display: block;
    margin-bottom: 1em;
    background: #eee;
    padding: 1em;
    border-radius: .15em;
}
nav a:hover{
    z-index: 2;
}
nav a:hover::before{
    background-color: #eee;
    margin-bottom: -1px;
}

饼图

一般在统计图表、进度指示器、定时器等场景中会用到饼图。我们希望能够很简单的创建出对应百分比的饼图,比如创建一个20%的饼图,只需一行代码,不引入外部插件。

<div class="pie">20%</div>

可以用下面这种方式解决。

基于transform的解决方案

首先要利用之前的border-radius创建一个圆,然后利用渐变遮盖半个圆。即:

<div class="pie-test"></div>
.pie-test{
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: yellowgreen;
    background-image: linear-gradient(to right,transparent 50%,#655 0);
}

然后设置伪元素,对右半圆进行遮盖

.pie-test::before{
    content: "";
    display: block;
    margin-left: 50%;
    height: 100%;
    border: 1px dashed black;
}

设置border只是为了演示方便

对于伪元素,有3件微小的工作需要它完成

  • 第一件,希望它能遮盖有半侧的黑色部分,设置background-color:inherit即可。
  • 第二件,希望它绕着圆形的圆心旋转,设置transform-origin: left或者0 50%
  • 第三件,不希望它是矩形的,可以设置.pie-test为overflow:hidden或者给伪元素设置合理的border-radius

伪元素:很惭愧,就做了三件微小的工作,谢谢大家!希望下次能控制天气~

如果想要显示比例20%,只要旋转.2turn即可

具体代码如下:

.pie-test::before{
    content: "";
    display: block;
    margin-left: 50%;
    height: 100%;
    border: 1px dashed black;
    background-color: inherit;
    transform-origin: left;
    border-radius: 0 100% 100% 0 / 50% 50%;
    transform: rotate(.2turn);
}

如果我们想要60%的比例,设置.6turn会出现下面的结果:

这时候可以把50%-100%看做另外一个问题,设置一个黑色的伪元素,让它在0-.5turn范围内旋转,此时,要得到一个60%比率的饼图,代码如下:

.pie-test::before{
    content: "";
    display: block;
    margin-left: 50%;
    height: 100%;
    border: 1px dashed black;
    background-color: #655;
    transform-origin: left;
    border-radius: 0 100% 100% 0 / 50%;
    transform: rotate(.1turn);
}

利用上面的代码可以用动画来做一个从0-100%的进度指示器了

这里给出完整代码:

<div class="pie-a"></div>
@keyframes spin{
    to {transform: rotate(.5turn);}
}
@keyframes bg{
    50% {background: #655;}
}
.pie-a{
    width: 100px;
    height: 100px;
    border-radius: 50%;
    background: yellowgreen;
    background-image: linear-gradient(to right,transparent 50%,#655 0);
}
.pie-a::before{
    content: '';
    display: block;
    margin-left: 50%;
    height: 100%;
    background-color: inherit;
    transform-origin: 0 50%;
    border-radius: 0 100% 100% 0 / 50%;
    animation: spin 3s linear infinite,
               bg 6s step-end infinite;
}

下面再来实现最初的需求,核心是利用动画。
一个负的延时值是合法的,它意味着动画会立即开始播放,但会自动前进到延时值的绝对处,这样在视觉上就好像动画跳过指定时间直接播放了。
然后再让它暂停到我们需要的位置即可。
我们可以设置一个长达100S的动画,便于计算。
代码如下:

<div class="pie">0%</div>
<div class="pie">40%</div>
<div class="pie">80%</div>
@keyframes spin{
    to {transform: rotate(.5turn);}
}
@keyframes bg{
    50% {background: #655;}
}
.pie{
    position: relative;
    display: inline-block;
    width: 100px;
    line-height: 100px;
    border-radius: 50%;
    background: yellowgreen;
    background-image: linear-gradient(to right,transparent 50%,#655 0);
    color: transparent;
}
.pie::before{
    content: '';
    position: absolute;
    top: 0;left: 50%;
    width: 50%;height: 100%;
    border-radius: 0 100% 100% 0 / 50%;
    background-color: inherit; 
    transform-origin: left;
    animation: spin 50s linear infinite,
               bg 100s step-end infinite;
    animation-play-state: paused;
    animation-delay: inherit;
}
<script type="text/javascript">
    function $$(selector,context){
        context = context || document;
        var elements = context.querySelectorAll(selector);
        return Array.prototype.slice.call(elements);
    }
    $$('.pie').forEach(function(pie){
        var p = pie.textContent;
        pie.style.animationDelay = '-' + parseFloat(p) + 's';
    });
</script>

SVG解决方案

还有一种是利用SVG来实现,主要原理的是stroke-dasharray属性,使它的虚线间隙超过圆的周长,利用描边来表示百分比。下面不在细说,直接给出实现代码:

<div class="pie-svg">20%</div>
<div class="pie-svg">80%</div>
<script type="text/javascript">
    function $$(selector,context){
        context = context || document;
        var elements = context.querySelectorAll(selector);
        return Array.prototype.slice.call(elements);
    }
    $$('.pie-svg').forEach(function(pie){
        var p = parseFloat(pie.textContent);
        var NS = "http://www.w3.org/2000/svg";
        var svg = document.createElementNS(NS, "svg");
        var circle = document.createElementNS(NS, "circle");
        var title = document.createElementNS(NS, "title");

        circle.setAttribute("r", 16);
        circle.setAttribute("cx", 16);
        circle.setAttribute("cy", 16);
        circle.setAttribute("stroke-dasharray", p + " 100");

        svg.setAttribute("viewBox", "0 0 32 32");
        title.textContent = pie.textContent;
        pie.textContent = '';
        svg.appendChild(title);
        svg.appendChild(circle);
        pie.appendChild(svg); 
    });
</script>


上面直接利用JavaScript完成自动化。也可以自己编写SVG标签。

svg{
    width: 100px;height: 100px;
    transform: rotate(-90deg);
    background: yellowgreen;
    border-radius: 50%;
}
circle{
    fill: yellowgreen;
    stroke: #655;
    stroke-width: 32;
}